# coding=UTF-8
'''
Core module for the SMSAppStore server. This module contains the following
classes:

``SMSAppStoreDispatcher`` - Receives messages from the SMS gateway and 
        handles the requests of the user (phone) that sent it.
         
``SessionHandler`` - Class that gathers the packets sent by a phone
        into sessions (a session in this context is an operation to
        be performed whose description was split over more than
        one packet) and executes them.

``_Session`` - Class that models a session, that is a list of
        packets sent by one client, that describe an operation to be performed.

Created on Jan 17, 2010

@author: Paul Lu
@author: Adolfo García
'''

import sql_utils
from sms_gateway import Message
from threading import Thread, RLock
import logging
import array
import Queue
import threading
import time
import codec
from schema_reader import FormHandler, AppHandler

_LOGGER = logging.getLogger("mx.org.came.core")

class SMSAppStoreDispatcher(Thread):
    """This class dispatches each SMS message received to the corresponding
    thread that handles the requests of the user (phone) that sent it.
    This class contains a queue in which instances of ``sms_gateway.Message``
    are inserted are inserted, then it determines if a SessionHandler thread
    for that number is active and delivers the message for it to handle.
        
    ``SMSAPPSTORE_UDH`` is the UDH (user data header) that identifies messages
    sent from the client application.
    """
    
    SMSAPPSTORE_UDH = "\x06\x05\x04\xdf\x8b\x00\x00"
    
    message_queue = Queue.Queue(0) #No size limit
    
    def __init__(self
                 , sms_gateway
                 , application_reader
                 , form_reader
                 , db_connection_factories
                 , session_handler_timeout = 120):
        """Creates a new instance of ``SMSAppStoreDispatcher`` that
        uses the instance of ``SMSGateway`` ``sms_gateway`` to
        send messages and receives them through the queue
        ``self.message_queue.
        
        In order to get the operation descriptions (i.e. what fields
        the message contains, how to interpret them, etc.) two XML files
        need to be read: the "application description" and the "form
        description". The ``application_reader`` and ``form_reader`` are
        in charge of getting these files. They are instances of
        ``ApplicationReader`` and ``FormReader`` respectively.
        
        To handle the operations described in the messages, queries have to be
        made. How to reach the database is specified through the
        ``db_connection_factories`` (a dictionary that maps the name of a
        database to its corresponding instance of
        ``springpython.database.factory.ConnectionFactory``).
        
        All the messages sent by each phone are
        handled by a specific ``SessionHandler`` which will stop
        waiting for messages (and drop all incomplete communications)
        after ``session_handler_timeout``. If ``session_handler_timeout``
        is set to ``None`` the ``SessionHandler`` will wait forever.
        """
        self._sms_gateway = sms_gateway
        self.application_reader = application_reader
        self.form_reader = form_reader
        self._db_connection_factories = db_connection_factories
        self._dispatching = False
        self._session_handler_timeout = session_handler_timeout
        self._is_shut_down = threading.Event()
        self._dispatch_thread_list_lock = RLock()
        Thread.__init__(self)
    
    def run(self):
        """
        Executes ``dispatch_forever()``.
        """
        self.dispatch_forever()
    
    def _get_session_handler(self, phone_number):
        """Returns the thread that handles requests for the given phone number.
        If no such thread exists, then a new instance of ``SessionHandler`` is
        returned.
        """
        session_thread = None
        with self._dispatch_thread_list_lock:
            threads = threading.enumerate()
            for t in threads:
                _LOGGER.debug("Thread: " + t.name)
                if t.name == phone_number:
                    session_thread = t
                    _LOGGER.debug("Found SessionHandler thread for "
                              + phone_number)
                    break
            if session_thread == None:
                _LOGGER.debug("Creating a new SessionHandler thread for phone: "
                              + phone_number)
                session_thread = SessionHandler(phone_number
                                        , self
                                        , self._session_handler_timeout)
                session_thread.daemon=True
                session_thread.start()
        return session_thread
    
    def dispatch_forever(self):
        """Dispatch one request at a time until shutdown. Retrieve an instance
        of ``sms_gateway.Message`` from the queue, determine if a SessionHandler
        thread for that number is active and deliver the message for it to
        handle.
        """
        self._dispatching = True
        _LOGGER.info("Started dispatching messages from the SMSAppStore "
                     + " client application")
        while self._dispatching:
            if not SMSAppStoreDispatcher.message_queue.empty() :
                _LOGGER.debug("Element was added to the message queue")
                message = SMSAppStoreDispatcher.message_queue.get()
                #===============================================================
                # Phone # will be the thread name
                # threading.enumerate() will return a list
                # contains all LIVE thread
                # i.e [<_MainThread(MainThread, started -1210562880)>, 
                #      <ELMR(Thread-1, started -1215906928)>, 
                #      <SessionHandler(9175629807, started -1224410224)>]
                # check is there any currently alive thread for Phone #
                #===============================================================
                _LOGGER.debug("Receiving Mesg form %s PKG= %s"
                              , message.data[Message.SENDER_PHONE_NUMBER]
                              , message.data[Message.ORIGINAL_BINARY_MESSAGE])
                session_thread = self._get_session_handler(
                        message.data[Message.SENDER_PHONE_NUMBER])
                session_thread.put_incoming_packet(
                        message.data[Message.ORIGINAL_BINARY_MESSAGE])
        # Notify dispatching has stopped
        self._is_shut_down.set()
    
    def shutdown(self):
        """Stops the serve_forever loop.

        Blocks until the loop has finished. This must be called while
        serve_forever() is running in another thread, or it will
        deadlock.
        """
        _LOGGER.debug("Shutting down SMSAppStore message dispatcher...")
        self._dispatching = False
        self._is_shut_down.wait()
        _LOGGER.info("SMSAppStore message dispatcher stopped.")


class SessionHandler(Thread):
    """``SessionHandler`` threads handle packets sent by a specific client,
    identified by its phone number. They contain a queue in which packets (the
    SMS's original binary messages) are stored. ``SessionHandler`` threads move
    the packets through the protocol stack and execute the requested operations
    once all the packets have been collected (one operation may span several
    packets).
    """
    
    def __init__(self
                 , phone_number
                 , dispatcher
                 , timeout = 120):
        """Creates a new instance of ``SessionHandler`` that will handle packets
        sent by ``phone_number`` using the gateway and other resources given to
        the SMSAppStoreDispatcher that created it (``dispatcher``). If no
        messages arrive after ``timeout`` seconds, the thread will stop. if it
        is set to ``None`` it will wait forever.
        
        To handle the operations described in the messages, queries have to be
        made (to get the application and form descriptions among other things)
        how to reach the database is specified through the ``dispatcher``'s
        ``db_connection_factories`` (a dictionary that maps the name of a
        database to its corresponding instance of
        ``springpython.database.factory.ConnectionFactory``). 
        """
        self._dispatcher = dispatcher
        self._timeout = timeout
        # Queue that holds the packets sent by the phone number 
        self.packet_queue = Queue.Queue(0)
        # Map that holds the active sessions (i.e. those for which we have not
        # received all the packets), having the session's ID as key associated
        # to the respective instance of _Session
        self._active_sessions = {}
        # invoke constructor of parent class
        Thread.__init__(self, name = phone_number)
    
    def run(self):
        """Gets packets from the queue associated to this SessionHandler
        and process them till no more messages are received and the timeout
        expires. The process the packets go through is:
        
        1.- They are gathered in the session they belong to till all
            of them are received. They may arrive in a different order
            than the one they were sent.
        
        2.- Once a session is complete, the process it describes is executed.
        """
        while True:
            packet = self._get_packet_as_byte_array()
            if (packet == None):
                _LOGGER.info("Retrieval of message exceeded timeout period. "
                             + "Terminating SessionHandler thread for %s"
                             , self.name)
                break
            self.add_packet_to_session(packet)
            session = self._get_session(_get_session_id(packet))
            if session.is_complete():
                self._process_session(session)
        _LOGGER.info("Closing SessionHandler thread form %s", self.name)
    
    def put_incoming_packet(self,pkg):
        _LOGGER.debug("Received a new packet")
        self.packet_queue.put(pkg)

    def _get_packet_as_byte_array(self):
        """Retrieves a packet from the queue ``packet_queue`` waiting for
        at most ``self._timeout`` seconds. If the queue is empty after that
        period, ``None`` is returned.
        """
        _LOGGER.debug("Get a new packet from Queue")
        packet = None
        try:
            packet = array.array('B'
                    , self.packet_queue.get(timeout = self._timeout))
        except Queue.Empty:
            _LOGGER.exception("No more messages from %s after %s seconds."
                              , self.name
                              , self._timeout)
        return packet
    
    def _get_session(self, session_id):
        """Returns the instance of ``_Session`` associated with the specified
        ID, if it exists. Otherwise, a KeyError is raised.
        """
        return self._active_sessions[session_id]
    
    def _set_session(self, session):
        """Sets the specified session in the map of sessions being handled.
        The value for ``session.id`` is set to ``session``.
        """
        self._active_sessions[session.id] = session
    
    def _contains_session(self, session_id):
        """Returns ``True`` if the session with the ID specified is currently
        being handled and ``False`` otherwise.
        """
        return self._active_sessions.has_key(session_id)

    def add_packet_to_session(self, packet):
        """Add the specified packet to the sessions being kept by the
        SessionHandler. If the session for such packet already exists,
        it is simply added at the proper position, otherwise, a new
        session is created.
        """
        _LOGGER.debug("Entering add_packet_to_session for session: %s"
                      , _get_session_id(packet))
        if self._contains_session(_get_session_id(packet)):
            _LOGGER.debug("Adding packet to existing session.")
            session = self._get_session(_get_session_id(packet))
            session.add_packet(packet)
        else:
            _LOGGER.debug("The packet is for a new session.")
            session = _Session(packet)
            self._set_session(session)
    
    def _create_packet(self,retArray, session):
        _LOGGER.debug("Entering _create_packet")
        pkgSize = _Session.SMS_SIZE - _Session.PACKET_CONTROL_HEADER_SIZE
        numPkgs = len(retArray) / pkgSize
        print len(retArray)
        print numPkgs
        print pkgSize
        if numPkgs % pkgSize !=0: numPkgs +=1
        if not numPkgs : numPkgs+=1
        for i in range(numPkgs):
            retArray.insert(0, i)
            retArray.insert(0, numPkgs)
            retArray.insert(0, session.id)
            retArray.insert(0, _Session.RESPONSE_PACKET_TYPE)
            print retArray
            if i == numPkgs-1: 
                sendArray=retArray[0:len(retArray)]
                for i in range(len(retArray)): retArray.pop(0)
                print sendArray
                # print retArray
            else: 
                sendArray=retArray[0:pkgSize]
                for i in range(pkgSize): retArray.pop(0)
                print sendArray
                #print retArray
            session.reply_packets.append(sendArray)
    
    def getFetchList(self,commonField,fieldList):
        fetch_list = []
        for id in commonField:
            r = int(fieldList[id]['range'])
            _LOGGER.debug("r: " + str(r))
            if r == AppHandler.SC_FIELD_RANGE or r == AppHandler.MC_FIELD_RANGE:
                chList=fieldList[id]['choice']
                for ch in chList:
                    fetch_list.append(ch+'({0})'.format(fieldList[id]['name']))	
            else:
                fetch_list.append(fieldList[id]['name'])
        _LOGGER.debug("fetch_list: " + str(fetch_list))
        return fetch_list

    def _send_sms(self, receiver_phone_number, byte_array):
        message = Message()
        message.data[Message.UDH] = SMSAppStoreDispatcher.SMSAPPSTORE_UDH
        message.data[Message.ORIGINAL_BINARY_MESSAGE] = byte_array.tostring()
        message.data[Message.SENDER_PHONE_NUMBER] = \
                self._dispatcher._sms_gateway.get_phone_number()
        message.data[Message.RECEIVER_PHONE_NUMBER] = receiver_phone_number
        self._dispatcher._sms_gateway.send_message(message)

    def _send_result_response(self, phone_number, message_data
                              , decoded_query, session):
        """Receives a string with the phone number to send the
        ``_Session.RESULT_RESPONSE_MESSAGE`` and a byte array ``data`` with
        the contents the message should have. Sends the reply in as many
        SMS messages as needed and 
        
        ``phone_number`` - string with the phone number to send the message to.
        
        ``data`` - byte array with the data to send in the message.
        
        ``decoded_query`` - a map containing the query's information, like
                            those returned by ``_decode_query()``
        
        ``session`` - The session to which the query being replied to belongs.
        """
        # Add the header to the message: RESPONSE_TYPE, APPLICATION_ID, FORM_ID
        message = array.array('B', message_data)
        message.insert(0, int(decoded_query["form_dict"]["id"]))
        message.insert(0, int(decoded_query["app_dict"]["id"]))
        message.insert(0, _Session.RESULT_RESPONSE_MESSAGE)
        self._create_packet(message, session)
        for packet_data in session.reply_packets:
            self._send_sms(phone_number, packet_data)
            # FIXME - Configure this elsewhere.
            time.sleep(15) # wait 15 seconds between messages
    
    def _send_error_message(self, phone_number, text_message, session):
        """Receives a string with the phone number to send the
        ``_Session.ERROR_RESPONSE_MESSAGE`` and a string ``text_message`` with
        the contents the message should have. Sends the error message in as many
        SMS messages as needed.
        
        ``phone_number`` - string with the phone number to send the message to.
        
        ``text_message`` - a short message describing the problem.
        
        ``session`` - The session for which the error occurred.
        """
        _LOGGER.info("%s : Operation failed. Sending notification message: %s"
                     , session.id
                     , text_message)
        message_data = array.array('B', text_message)
        message_data.insert(0, len(message_data))
        message_data.insert(0, _Session.ERROR_RESPONSE_MESSAGE)
        self._create_packet(message_data, session)
        for packet_data in session.reply_packets:
            self._send_sms(phone_number, packet_data)
            # FIXME - Configure this elsewhere.
            time.sleep(15) # wait 15 seconds between messages
    
    def _process_session(self, session):
        """Performs the operation specified in the session and
        sends the reply (if any) to the client.
        """
        # A session contains one transaction and a transaction may
        # contain many queries, but only one with a result to be sent back.
        query_with_results = None
        try:
            # Process the transaction and get the query with results to send
            # back along with its results (if such a query is in the
            # transaction)
            query_with_results = self._process_transaction(session)
        except Exception, e:
            _LOGGER.exception("While processing query: %s", e)
            self._send_error_message(self.name
                                     , "Error processing request."
                                     , session)
        if query_with_results != None:
            # Encode the reply (i.e. get the data to send back)
            encoded_result = codec.encode_fetch_result(
                    query_with_results["result_set"]
                    , query_with_results["app_dict"]
                    , query_with_results["form_dict"])
            self._send_result_response(self.name, encoded_result
                                       , query_with_results, session)
        # Remove session after the reply (if any) has been sent
        self._active_sessions.pop(session.id)
    
    def _process_transaction(self, session):
        """Executes the queries in the session's transaction and if one of them
        generates a result, returns that decoded query along with its result
        set in a dictionary with the following keys:
        
        ``result_set`` - The query's results. A list containing tuples.
        
        ``decoded_query`` - Dictionary returned by ``codec.decode()``
        
        ``app_dict`` - Dictionary for the query's application descriptor.
        
        ``form_dict`` -  Dictionary for the query's form descriptor.
        
        Note: Each session contains one "transaction" (read *operation list*,
        no atomicity is warranted) with one or more queries.
        This method retrieves the queries in the "transaction", decodes them
        and executes them. At most one of the queries can generate a response
        (as in a SELECT statement).
        """
        _LOGGER.debug( "Entering _process_transaction")
        # Only the last query_enc can contain a response, so no
        # response messages can be lost.
        query_with_results = None
        for packet in session.received_packets:
            encoded_queries = self._get_encoded_queries_from_packet(packet)
            for query_enc in encoded_queries:
                query = self._decode_query(query_enc)
                result_set = self._process_decoded_query(session = session
                                                         , **query)
                if result_set != None:
                    query['result_set'] = result_set
                    query_with_results = query
        _LOGGER.debug( "%s: Transaction result: %s"
                       , session.id, query_with_results)
        return query_with_results
    
    def _get_encoded_queries_from_packet(self, packet):
        """Receives a packet of type ``_Session.SESSION_PACKET_TYPE``.
        containing queries and returns a list of byte arrays, one for each
        query. Each array of bytes contains the encoded data for one query. 
        """
        transaction_data = packet[_Session.PACKET_CONTROL_HEADER_SIZE:]
        # The first byte contains the transaction ID
        transaction_id = transaction_data.pop(0)
        # The second byte contains the number of queries in the transaction
        # FIXME - What's the use of Transaction ID?! It is never used.
        number_of_queries = transaction_data.pop(0)
        _LOGGER.debug("There are %s queries in this transaction. TID=%s"
                      , number_of_queries
                      , transaction_id)
        queries = []
        i = 0
        while i < number_of_queries and transaction_data:
            #Each query is preceded by the number of bytes it takes
            query_length = transaction_data.pop(0)
            queries.append(transaction_data[0 : query_length])
            transaction_data = transaction_data[query_length:]
            i += 1
        if i < number_of_queries:
            _LOGGER.error("There are less queries than expected! Expected: %s"
                          + " Actual %s", number_of_queries, i)
        _LOGGER.debug("Queries to perform: %s", queries)
        return queries

    def _decode_query(self, encoded_query):
        """Receives a byte array containing a valid encoded query and returns
        a dictionary with keys: ``(decoded_query``, ``app_dict`` and
        ``form_dict``; where ``decoded_query``'s value is the dictionary
        returned by
        ``codec.decode(app_dictionary, form_dictionary, query_byte_array)``,
        ``app_dict`` is the dictionary for the query's application descriptor
        and ``form_dict`` is the dictionary for the query's form descriptor.
        As a side effect, the first two bytes in ``encoded_query`` are removed
        (those that specify the application and form descriptor's ID.
        """
        # The application ID is in the first byte
        app_dict = self._dispatcher.application_reader.parseFile(
                encoded_query.pop(0))
        # The form ID is in the second byte
        form_dict = self._dispatcher.form_reader.parseFile(
                app_dict["id"]
                ,encoded_query.pop(0))
        query_dict = codec.decode(app_dict, form_dict, encoded_query)
        _LOGGER.debug("decoded_query: %s", query_dict)
        return {'decoded_query' : query_dict
                , 'app_dict' : app_dict
                , 'form_dict' : form_dict}

    def _process_decoded_query(self, decoded_query, app_dict, form_dict
                               , session):
        """Generates and executes the SQL for the specified decoded query
        ``decoded_query``. ``app_dict`` is the dictionary for the query's
        application descriptor and ``forrm_dict`` the dictionary for its
        form descriptor. If the query is a selection, a list of tuples is
        returned, otherwise None is returned. If an error occurs while
        executing the query, an exception will be raised.
        """
        # Get the database connection factory that will be used to perform the
        # queries.
        db_name = decoded_query.pop('dbName')
        try:
            db_connection_factory = self._dispatcher._db_connection_factories[db_name]
        except KeyError, e:
            _LOGGER.exception("No database connection factory was found for: %s"
                          , db_name)
            raise e
        _LOGGER.debug("Using database connection factory for: %s"
                          , db_name)
        operation_code = int(form_dict['operation'])
        
        _LOGGER.debug("%s : Operation code: "
                          , session.id
                          , operation_code)
        # Dictionary with the parameters needed to invoke run_script
        sql_dict = None 
        if operation_code == FormHandler.OP_CAME_PAYMENT:
            sql_dict = {'sql' :  sql_utils.get_insert_sql(decoded_query)
                        , 'is_fetch' : False}
        elif operation_code == FormHandler.OP_CREATE_ID :
            sql_dict = {'sql' : sql_utils.get_insert_sql(decoded_query)
                        , 'is_fetch' : False}
        elif operation_code == FormHandler.OP_UPDATE_ID :
            sql_dict = {'sql' : sql_utils.get_insert_sql(decoded_query)
                        , 'is_fetch' : False}
        elif operation_code == FormHandler.OP_DESTROY_ID :
            sql_dict = {'sql' : sql_utils.get_delete_sql(decoded_query)
                        , 'is_fetch' : False}
        elif operation_code == FormHandler.OP_SEARCH_ID :
            schema_id = form_dict['schemas'].keys()[0]
            fetch_list = self.getFetchList(
                    form_dict['schemas'][schema_id]['fetch']
                    , app_dict['schemas'][schema_id])
            logical = decoded_query.pop('logical')
            sql_dict = {'sql' : sql_utils.get_select_sql(decoded_query
                                                      , fetch_list
                                                      , logical)
                         , 'is_fetch' : True}
        elif operation_code == FormHandler.OP_ADV_SEARCH_ID :
            # FIXME - This operation has not been implemented!
            sql_dict = None
        else:
            _LOGGER.error("%s : Unrecognized operation code: "
                          , session.id
                          , operation_code)
            raise KeyError("Unrecognized operation code: " 
                           + str(operation_code))
        result_set = None
        try:
            sql_runner = sql_utils.SQLScriptRunner(db_connection_factory)
            result_set = sql_runner.run_script(**sql_dict)
        except Exception, e:
            _LOGGER.exception("While performing query: %s", e)
            raise e
        _LOGGER.debug("%s: Query results: %s", session.id, result_set)
        return result_set

class _Session:
    """Models a session for ``SessionHandler``. In this context, a session is
    an operation that may have a definition that spans several packets. Each
    operation has its own session. The packets have a four byte header
    made up of:
    
    +------------------+-----------------+-----------------------------------+------------------------------+
    | PACKET_TYPE_BYTE | SESSION_ID_BYTE | NUMBER_OF_PACKETS_IN_SESSION_BYTE | PACKET_INDEX_IN_SESSION_BYTE |
    +------------------+-----------------+----------+------------------------+------------------------------+
    """
    
    # Number of bytes in the packet's header with control information. These
    # are the first bites in the header. The first byte indicates the type of
    # the packet, the second the session ID, the third the total number of
    # packets in the session and the fourth the index of the packet in the
    # sequence that makes up the session.
    PACKET_CONTROL_HEADER_SIZE = 4

    
    # Index of the byte that holds the type of the packet packets (inside the
    # byte array that makes up a packet).
    PACKET_TYPE_BYTE = 0
    
    # Index of the byte that holds the number of packets that make up the
    # session the packet belongs to.
    SESSION_ID_BYTE = 1
    
    # Index of the byte that holds the number of packets that make up the
    # session the packet belongs to.
    NUMBER_OF_PACKETS_IN_SESSION_BYTE = 2
    
    # Index of the byte that holds the index of the packet in a session, inside
    # a packet (represented by an array of bytes).
    PACKET_INDEX_IN_SESSION_BYTE = 3
    
    # Possible packet types sent from client to server

    # Value that identifies the packets of type *session* inside the
    # ``PACKET_TYPE_BYTE``.
    SESSION_PACKET_TYPE = 1

    # Possible packet types sent from server to client.
    
    # Value that identifies the packets of type *response* inside the
    # ``PACKET_TYPE_BYTE``.
    RESPONSE_PACKET_TYPE = 3
    
    # Identifies a response message (i.e. response session) that does not
    # include results for an operation requested by the client (i.e. it's
    # empty). It indicate an error while performing a request.
    ERROR_RESPONSE_MESSAGE = 0
    
    # Identifies a response message (i.e. response session) that includes
    # results for an operation requested by the client. 
    RESULT_RESPONSE_MESSAGE = 1


    # Maximum number of bytes that may be used to store information in a single
    # SMS message. The total number of bytes in an SMS message is 140 this
    # includes the SMS's header and its payload. Inside the payload, additional
    # header information for SMSAppStore is included.
    # 
    # 11 byte => SMS Header 4 byte => session header
    SMS_SIZE = 130 

    def __init__(self, packet):
        """Creates a new instance of ``_Session`` based on the information
        provided in ``packet``. ``packet`` is a byte array where the third
        byte indicates the number of packets that make up the operation,
        the fourth which packet in the sequence this is.
        """
        # packet[1] = ID of the session this packet belongs to
        # packet[2] = Total number of packets that make up the operation.
        # packet[3] = Index of this packet in the sequence.
        self.id = _get_session_id(packet)
        # _received_packets - List with as many entries as packets
        self.received_packets = [[]] * _get_total_packets_in_session(packet)
        self.received_packets[_get_packet_index(packet)] = packet
        # The packets to be sent as reply
        self.reply_packets = []

    def add_packet(self, packet):
        """Adds the packet to the ``Session``'s ``received_packets`` at
        the appropriate position.
        """
        self.received_packets[_get_packet_index(packet)] = packet
    
    def is_complete(self):
        """Returns ``True`` if all the packets for this session have arrived.
        That is, they are registered in received_packets.
        """
        packets_missing = False
        for packet in self.received_packets :
            if not packet: # if packet == []
                packets_missing = True
                break
        return not packets_missing    

def _get_packet_type(packet):
    """Returns the packet's type. That is, the nature of its contents.
    ``packet`` is a byte array that contains the raw binary data contained in
    the SMS message.
    """
    return packet[_Session.PACKET_TYPE_BYTE]

def _get_session_id(packet):
    """Returns the packet's session ID. ``packet`` is a byte array that
    contains the raw binary data contained in the SMS message.
    """
    return packet[_Session.SESSION_ID_BYTE]

def _get_total_packets_in_session(packet):
    """Returns the total number of packets that make up the session.
    ``packet`` is a byte array that contains the raw binary data contained in
    the SMS message.
    """
    return packet[_Session.NUMBER_OF_PACKETS_IN_SESSION_BYTE]

def _get_packet_index(packet):
    """Returns this packet's index in the sequence that makes up the
    session.
    """
    return packet[_Session.PACKET_INDEX_IN_SESSION_BYTE]
